package com.hundsun.documentripper.controller;

import com.github.difflib.DiffUtils;
import com.github.difflib.patch.AbstractDelta;
import com.github.difflib.patch.Chunk;
import com.github.difflib.patch.DeltaType;
import com.hundsun.documentripper.domain.dto.BaseResponse;
import com.hundsun.documentripper.domain.dto.NodeTemplateView;
import com.hundsun.documentripper.domain.other.compare.CompareResponse;
import com.hundsun.documentripper.domain.other.compare.Context;
import com.hundsun.documentripper.domain.other.compare.Line;
import com.hundsun.documentripper.domain.pojo.File;
import com.hundsun.documentripper.domain.pojo.FileHistory;
import com.hundsun.documentripper.domain.pojo.FileNode;
import com.hundsun.documentripper.domain.pojo.NodeContent;
import com.hundsun.documentripper.error.BaseException;
import com.hundsun.documentripper.service.FileNodeService;
import com.hundsun.documentripper.service.FileService;
import com.hundsun.documentripper.service.NodeContentService;
import com.hundsun.documentripper.service.NodeTemplateService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFColor;
import org.apache.poi.xssf.usermodel.XSSFFont;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.util.UriUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 对比
 */
@Api("对比")
@Controller
public class ComparisonController {
    private static final String TITLE = "title";

    private static final String CONTENT = "content";

    private static final Line EMPTY_LINE = new Line();

    @Setter(onMethod_ = @Autowired)
    private FileService fileService;

    @Setter(onMethod_ = @Autowired)
    private FileNodeService fileNodeService;

    @Setter(onMethod_ = @Autowired)
    private NodeContentService nodeContentService;

    @Setter(onMethod_ = @Autowired)
    private NodeTemplateService nodeTemplateService;

    @ApiOperation("对比两个文件")
    @GetMapping("/compare")
    @ResponseBody
    public BaseResponse<CompareResponse> compare(
            @RequestParam("originalFileUuid") String originalFileUuid,
            @RequestParam("revisedFileUuid") String revisedFileUuid) {
        Context context = process(originalFileUuid, revisedFileUuid);

        List<String> nodeTemplateUuidList =
                Stream.concat(
                        context.originalFileNodeList.stream(),
                        context.revisedFileNodeList.stream())
                        .flatMap((item) -> Stream.of(
                                item.getFileNodeTemplateUuid(),
                                item.getContentNodeTemplateUuid()))
                        .filter(StringUtils::isNotEmpty)
                        .distinct()
                        .collect(Collectors.toCollection(() -> new ArrayList<>(
                                context.originalFileNodeList.size() +
                                        context.revisedFileNodeList.size())));

        List<NodeTemplateView> nodeTemplateViewList = nodeTemplateService.getNodeTemplateView(nodeTemplateUuidList);

        CompareResponse compareResponse = new CompareResponse();
        compareResponse.originalFile = context.originalFile;
        compareResponse.revisedFile = context.revisedFile;
        compareResponse.originalFileNodeList = context.originalFileNodeList;
        compareResponse.revisedFileNodeList = context.revisedFileNodeList;
        compareResponse.originalLineList = context.originalLineList;
        compareResponse.revisedLineList = context.revisedLineList;
        compareResponse.nodeTemplateViewList = nodeTemplateViewList;
        compareResponse.deltaTypeList = context.deltaTypeList;
        return new BaseResponse<>(compareResponse);
    }

    @ApiOperation("对比两个文件，并导出文件")
    @GetMapping("/compareToExcel")
    public void compareToExcel(
            @RequestParam("originalFileUuid") String originalFileUuid,
            @RequestParam("revisedFileUuid") String revisedFileUuid,
            HttpServletResponse httpServletResponse) throws IOException {
        Context context = process(originalFileUuid, revisedFileUuid);

        List<Line> originalLineList = context.originalLineList;
        List<Line> revisedLineList = context.revisedLineList;

        List<String> originalParagraphNumberList = getParagraphNumberList(
                originalLineList,
                context.originalFileNodeList,
                context.originalFileNodeGrouped);
        List<String> revisedParagraphNumberList = getParagraphNumberList(
                revisedLineList,
                context.revisedFileNodeList,
                context.revisedFileNodeGrouped);

        XSSFWorkbook workbook = new XSSFWorkbook();

        XSSFFont font = workbook.createFont();
        font.setBold(true);
        font.setFontName("微软雅黑");

        XSSFCellStyle headerStyle = workbook.createCellStyle();
        headerStyle.setFont(font);
        headerStyle.setBorderTop(BorderStyle.THIN);
        headerStyle.setBorderRight(BorderStyle.THIN);
        headerStyle.setBorderBottom(BorderStyle.THIN);
        headerStyle.setBorderLeft(BorderStyle.THIN);

        XSSFCellStyle headerDeltaStyle = headerStyle.copy();
        headerDeltaStyle.setAlignment(HorizontalAlignment.CENTER);

        font = workbook.createFont();
        font.setFontName("微软雅黑");

        XSSFCellStyle equalStyle = headerStyle.copy();
        equalStyle.setFont(font);

        XSSFCellStyle equalContentStyle = equalStyle.copy();
        equalContentStyle.setWrapText(true);

        XSSFCellStyle equalDeltaStyle = equalStyle.copy();
        equalDeltaStyle.setAlignment(HorizontalAlignment.CENTER);

        font = workbook.createFont();
        font.setFontName("微软雅黑");
        font.setColor(new XSSFColor(new byte[]{(byte) 156, (byte) 87, (byte) 0}, null));

        XSSFCellStyle changeStyle = equalStyle.copy();
        changeStyle.setFont(font);
        changeStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte) 255, (byte) 235, (byte) 156}, null));
        changeStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        XSSFCellStyle changeContentStyle = changeStyle.copy();
        changeContentStyle.setWrapText(true);

        XSSFCellStyle changeDeltaStyle = changeStyle.copy();
        changeDeltaStyle.setAlignment(HorizontalAlignment.CENTER);

        font = workbook.createFont();
        font.setFontName("微软雅黑");
        font.setColor(new XSSFColor(new byte[]{(byte) 0, (byte) 97, (byte) 0}, null));

        XSSFCellStyle insertStyle = changeStyle.copy();
        insertStyle.setFont(font);
        insertStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte) 198, (byte) 239, (byte) 206}, null));

        XSSFCellStyle insertContentStyle = insertStyle.copy();
        insertContentStyle.setWrapText(true);

        XSSFCellStyle insertDeltaStyle = insertStyle.copy();
        insertDeltaStyle.setAlignment(HorizontalAlignment.CENTER);

        font = workbook.createFont();
        font.setFontName("微软雅黑");
        font.setColor(new XSSFColor(new byte[]{(byte) 156, (byte) 0, (byte) 6}, null));

        XSSFCellStyle deleteStyle = changeStyle.copy();
        deleteStyle.setFont(font);
        deleteStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte) 255, (byte) 199, (byte) 206}, null));

        XSSFCellStyle deleteContentStyle = deleteStyle.copy();
        deleteContentStyle.setWrapText(true);

        XSSFCellStyle deleteDeltaStyle = deleteStyle.copy();
        deleteDeltaStyle.setAlignment(HorizontalAlignment.CENTER);

        Sheet sheet = workbook.createSheet("比对结果");
        sheet.setAutoFilter(new CellRangeAddress(0, 0, 2, 2));
        sheet.createFreezePane(0, 1);
        sheet.setColumnWidth(0, 3072);
        sheet.setColumnWidth(1, 16384);
        sheet.setColumnWidth(2, 2048);
        sheet.setColumnWidth(3, 16384);
        sheet.setColumnWidth(4, 3072);

        Row row = sheet.createRow(0);

        Cell cell = row.createCell(0);
        cell.setCellValue("基础节号");
        cell.setCellStyle(headerStyle);
        cell = row.createCell(1);
        cell.setCellValue("基础版本 - " + context.originalFile.getName());
        cell.setCellStyle(headerStyle);
        cell = row.createCell(2);
        cell.setCellValue("差异");
        cell.setCellStyle(headerDeltaStyle);
        cell = row.createCell(3);
        cell.setCellValue("目标版本 - " + context.revisedFile.getName());
        cell.setCellStyle(headerStyle);
        cell = row.createCell(4);
        cell.setCellValue("目标节号");
        cell.setCellStyle(headerStyle);

        List<DeltaType> deltaTypeList = context.deltaTypeList;

        String currentOriginalNodeUuid = null;
        String currentRevisedNodeUuid = null;
        for (int i = 0, originalLineListSize = originalLineList.size(); i < originalLineListSize; i++) {
            CellStyle style = null;
            CellStyle contentStyle = null;
            CellStyle deltaStyle = null;
            String deltaString = null;
            switch (deltaTypeList.get(i)) {
                case EQUAL:
                    style = equalStyle;
                    contentStyle = equalContentStyle;
                    deltaStyle = equalDeltaStyle;
                    deltaString = "相等";
                    break;
                case CHANGE:
                    style = changeStyle;
                    contentStyle = changeContentStyle;
                    deltaStyle = changeDeltaStyle;
                    deltaString = "更改";
                    break;
                case INSERT:
                    style = insertStyle;
                    contentStyle = insertContentStyle;
                    deltaStyle = insertDeltaStyle;
                    deltaString = "插入";
                    break;
                case DELETE:
                    style = deleteStyle;
                    contentStyle = deleteContentStyle;
                    deltaStyle = deleteDeltaStyle;
                    deltaString = "删除";
                    break;
            }

            Line originalLine = originalLineList.get(i);
            Line revisedLine = revisedLineList.get(i);

            row = sheet.createRow(i + 1);

            cell = row.createCell(0);
            cell.setCellStyle(style);
            if (originalLine.nodeUuid != null && !originalLine.nodeUuid.equals(currentOriginalNodeUuid)) {
                currentOriginalNodeUuid = originalLine.nodeUuid;
                cell.setCellValue(originalParagraphNumberList.get(i));
            }

            cell = row.createCell(1);
            cell.setCellStyle(contentStyle);
            cell.setCellValue(originalLine.content);

            cell = row.createCell(2);
            cell.setCellStyle(deltaStyle);
            cell.setCellValue(deltaString);

            cell = row.createCell(3);
            cell.setCellStyle(contentStyle);
            cell.setCellValue(revisedLine.content);

            cell = row.createCell(4);
            cell.setCellStyle(style);
            if (revisedLine.nodeUuid != null && !revisedLine.nodeUuid.equals(currentRevisedNodeUuid)) {
                currentRevisedNodeUuid = revisedLine.nodeUuid;
                cell.setCellValue(revisedParagraphNumberList.get(i));
            }
        }

        // 支持中文文件名，根据RFC-5987，文件名使用URI编码，因此不能使用URLEncoder，否则文件名中的空格会被处理成加号
        // Character Set and Language Encoding for Hypertext Transfer Protocol (HTTP) Header Field Parameters
        // https://tools.ietf.org/html/rfc5987
        // Uniform Resource Identifier (URI): Generic Syntax
        // https://tools.ietf.org/html/rfc3986
        // 这里使用Spring的UriUtils提供的轻量化的解决方案，JDK中的URI太麻烦了
        httpServletResponse.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + UriUtils.encode("对比报告-" + context.originalFile.getName() + "-" + context.revisedFile.getName() + ".xlsx", StandardCharsets.UTF_8.name()));
        httpServletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        workbook.write(httpServletResponse.getOutputStream());
    }

    private List<String> getParagraphNumberList(List<Line> lineList, List<FileNode> fileNodeList, Map<String, List<FileNode>> fileNodeGrouped) {
        Map<String, FileNode> fileNodeMap = fileNodeList.stream()
                .collect(Collectors.toMap(
                        FileNode::getUuid,
                        Function.identity()));

        List<String> paragraphNumberList = new ArrayList<>(Collections.nCopies(lineList.size(), null));

        Map<String, String> uuidParagraphNumberCache = new HashMap<>();
        for (int i = 0, lineListSize = lineList.size(); i < lineListSize; i++) {
            Line line = lineList.get(i);
            if (line.nodeUuid == null) {
                continue;
            }

            StringBuilder stringBuilder = new StringBuilder();
            String currentUuid = line.nodeUuid;
            do {
                String cached = uuidParagraphNumberCache.get(currentUuid);
                if (cached != null) {
                    stringBuilder.insert(0, cached);
                    break;
                }

                FileNode fileNode = fileNodeMap.get(currentUuid);
                String parentUuid = fileNode.getParentUuid();
                if (parentUuid != null && parentUuid.isEmpty()) {
                    parentUuid = null;
                }

                stringBuilder.insert(0, fileNodeGrouped.get(parentUuid).indexOf(fileNode) + 1);
                if (parentUuid != null) {
                    stringBuilder.insert(0, '.');
                }
                currentUuid = parentUuid;
            } while (currentUuid != null);

            String paragraphNo = stringBuilder.toString();
            uuidParagraphNumberCache.putIfAbsent(line.nodeUuid, paragraphNo);
            paragraphNumberList.set(i, paragraphNo);
        }

        return paragraphNumberList;
    }

    private Context process(String originalFileUuid, String revisedFileUuid) {
        // 查询File与FileNode
        File originalFile = fileService.getFileById(originalFileUuid);
        File revisedFile = fileService.getFileById(revisedFileUuid);
        if (originalFile == null || revisedFile == null) {
            throw new BaseException("4003");
        }

        List<FileNode> originalFileNodeList;
        if (originalFile instanceof FileHistory) {
            // noinspection unchecked,rawtypes
            originalFileNodeList = (List<FileNode>) (List) fileNodeService.getFileNodeHistoryListById(originalFileUuid);
        } else {
            originalFileNodeList = fileNodeService.getFileNodeListById(originalFileUuid);
        }

        List<FileNode> revisedFileNodeList;
        if (revisedFile instanceof FileHistory) {
            // noinspection unchecked,rawtypes
            revisedFileNodeList = (List<FileNode>) (List) fileNodeService.getFileNodeHistoryListById(revisedFileUuid);
        } else {
            revisedFileNodeList = fileNodeService.getFileNodeListById(revisedFileUuid);
        }

        // 组装行列表
        List<Line> originalLineList;
        List<Line> revisedLineList;
        Map<String, List<FileNode>> originalFileNodeGrouped = new HashMap<>();
        Map<String, List<FileNode>> revisedFileNodeGrouped = new HashMap<>();
        {
            // 查询两文件的NodeContent
            HashSet<String> contentUuidSet = new HashSet<>((int) ((originalFileNodeList.size() + revisedFileNodeList.size() + 1) / 0.75));
            Stream.concat(originalFileNodeList.stream(), revisedFileNodeList.stream())
                    .map(FileNode::getContentUuid)
                    .filter(StringUtils::isNotEmpty)
                    .collect(Collectors.toCollection(() -> contentUuidSet));

            List<NodeContent> nodeContentList = nodeContentService.getNodeContentList(new ArrayList<>(contentUuidSet));
            HashMap<String, String> nodeContentMap = nodeContentList.stream()
                    .collect(Collectors.toMap(
                            NodeContent::getUuid,
                            NodeContent::getValue,
                            (oldValue, newValue) -> newValue,
                            () -> new HashMap<>((int) ((nodeContentList.size() + 1) / 0.75))));

            // 组装用于比较的行列表
            originalLineList = getLineList(sortLikeTree(originalFileNodeList, originalFileNodeGrouped), nodeContentMap);
            revisedLineList = getLineList(sortLikeTree(revisedFileNodeList, revisedFileNodeGrouped), nodeContentMap);
        }

        // 生成差异信息
        List<AbstractDelta<String>> deltaList = DiffUtils.diff(
                originalLineList.stream()
                        .map(item -> item.content)
                        .collect(Collectors.toCollection(() ->
                                new ArrayList<>(originalLineList.size()))),
                revisedLineList.stream()
                        .map(item -> item.content)
                        .collect(Collectors.toCollection(() ->
                                new ArrayList<>(revisedLineList.size()))))
                .getDeltas();
        // 浅拷贝并反转
        deltaList = new ArrayList<>(deltaList);
        Collections.reverse(deltaList);

        // 准备差异状态列表
        List<DeltaType> deltaTypeList = new ArrayList<>(Collections.nCopies(originalLineList.size(), DeltaType.EQUAL));

        // 将差异信息倒序应用至行
        for (AbstractDelta<String> delta : deltaList) {
            Chunk<String> source = delta.getSource();
            Chunk<String> target = delta.getTarget();
            Line[] lineArray;
            switch (delta.getType()) {
                case CHANGE:
                    int max = Math.max(source.size(), target.size());
                    processChange(source, originalLineList, max, deltaTypeList);
                    processChange(target, revisedLineList, max, null);

                    break;
                case INSERT:
                    lineArray = new Line[target.size()];
                    Arrays.fill(lineArray, EMPTY_LINE);
                    originalLineList.addAll(source.getPosition(), Arrays.asList(lineArray));

                    DeltaType[] deltaTypeArray = new DeltaType[lineArray.length];
                    Arrays.fill(deltaTypeArray, DeltaType.INSERT);
                    deltaTypeList.addAll(source.getPosition(), Arrays.asList(deltaTypeArray));

                    break;
                case DELETE:
                    lineArray = new Line[source.size()];
                    Arrays.fill(lineArray, EMPTY_LINE);
                    revisedLineList.addAll(target.getPosition(), Arrays.asList(lineArray));

                    for (int i = source.getPosition(), maxIndex = i + source.size(); i < maxIndex; i++) {
                        deltaTypeList.set(i, DeltaType.DELETE);
                    }

                    break;
            }
        }

        Context context = new Context();
        context.originalFile = originalFile;
        context.revisedFile = revisedFile;
        context.originalFileNodeList = originalFileNodeList;
        context.revisedFileNodeList = revisedFileNodeList;
        context.originalFileNodeGrouped = originalFileNodeGrouped;
        context.revisedFileNodeGrouped = revisedFileNodeGrouped;
        context.originalLineList = originalLineList;
        context.revisedLineList = revisedLineList;
        context.deltaTypeList = deltaTypeList;
        return context;
    }

    private void processChange(Chunk<String> chunk, List<Line> lineList, int max, List<DeltaType> deltaTypeList) {
        if (deltaTypeList != null) {
            for (int i = chunk.getPosition(), maxIndex = i + chunk.size(); i < maxIndex; i++) {
                deltaTypeList.set(i, DeltaType.CHANGE);
            }
        }

        if (chunk.size() < max) {
            int insertIndex = chunk.getPosition() + chunk.size();

            Line[] lineArray = new Line[max - chunk.size()];
            Arrays.fill(lineArray, EMPTY_LINE);
            lineList.addAll(insertIndex, Arrays.asList(lineArray));

            if (deltaTypeList != null) {
                DeltaType[] deltaTypeArray = new DeltaType[lineArray.length];
                Arrays.fill(deltaTypeArray, DeltaType.CHANGE);
                deltaTypeList.addAll(insertIndex, Arrays.asList(deltaTypeArray));
            }
        }
    }

    /**
     * 获取行列表
     */
    private List<Line> getLineList(List<FileNode> fileNodeList, Map<String, String> nodeContentMap) {
        List<Line> lineList = new ArrayList<>(fileNodeList.size() * 2);
        for (FileNode fileNode : fileNodeList) {
            String uuid = fileNode.getUuid();
            partToLine(lineList, uuid, fileNode.getNodeValue(), TITLE);
            partToLine(lineList, uuid, nodeContentMap.get(fileNode.getContentUuid()), CONTENT);
        }
        return lineList;
    }

    private void partToLine(List<Line> lineList, String nodeUuid, String partContent, String part) {
        if (StringUtils.isNotEmpty(partContent)) {
            for (String lineContent : partContent.split("\n")) {
                Line line = new Line();
                line.content = lineContent;
                line.nodeUuid = nodeUuid;
                line.part = part;
                lineList.add(line);
            }
        }
    }

    /**
     * 将FileNode转树、排序、按先根深度优先遍历
     */
    private List<FileNode> sortLikeTree(List<FileNode> fileNodeList, Map<String, List<FileNode>> fileNodeGrouped) {
        // 按parentUuid分组
        for (FileNode fileNode : fileNodeList) {
            String parentUuid = fileNode.getParentUuid();
            if (parentUuid != null && parentUuid.isEmpty()) {
                parentUuid = null;
            }
            fileNodeGrouped
                    .computeIfAbsent(parentUuid, (key) -> new ArrayList<>())
                    .add(fileNode);
        }

        // 如果没有根节点，则返回空
        if (fileNodeGrouped.get(null) == null) {
            return Collections.emptyList();
        }

        // 对树排序，倒排
        for (List<FileNode> fileNodeGroup : fileNodeGrouped.values()) {
            fileNodeGroup.sort(Comparator.comparingInt(FileNode::getNodeOrder).reversed());
        }

        // 添加根节点入栈，由于是倒排，此时栈顶是nodeOrder为0的节点
        Deque<FileNode> stack = new ArrayDeque<>(fileNodeList.size());
        for (FileNode fileNode : fileNodeGrouped.get(null)) {
            stack.addFirst(fileNode);
        }

        fileNodeList = new ArrayList<>(fileNodeList.size());
        while (!stack.isEmpty()) {
            // 取出栈顶节点
            FileNode fileNode = stack.removeFirst();
            // 输出
            fileNodeList.add(fileNode);

            // 取出children入栈，由于children是倒排，此时栈顶是children中nodeOrder为0的节点
            List<FileNode> children = fileNodeGrouped.get(fileNode.getUuid());
            if (children != null) {
                for (FileNode child : children) {
                    stack.addFirst(child);
                }
            }
        }

        // 反转树的顺序，正排
        for (List<FileNode> fileNodeGroup : fileNodeGrouped.values()) {
            Collections.reverse(fileNodeGroup);
        }

        return fileNodeList;
    }
}
